今天會帶大家使用 unsloth
這個好用的 library ,在單張消費級顯卡上微調自己的大語言模型🚀🚀!
Day 17我們結合多種不同資料來源與技巧,生成了增強版的訓練數據;Day 18 我們深入研究評測通用型 LLM 的常見 Benchmark 以及 Leaderboard,知道該如何挑選適合我們領域任務的大語言模型後,今天就要開始用我們自己的 Science Exam 資料來 fine-tune 一個答題LLM囉~
由於 LLM 動輒幾十億、幾百億的參數,所以微調 LLM 和微調 BERT
, Deberta
等模型相比會更複雜一些。我們在開始訓練 LLM 之前,需要先知道兩件事情- Quantization(量化) 與 LoRA & QLoRA 技術。
大多數模型本身使用32位浮點數(通常稱為全精度)表示,假設我們現在有一個包含700億參數的模型,這時就需要280GB的VRAM來加載模型。但如果能將所有參數用16位浮點數表示,所需的內存大小就可以直接減少一倍。
因此,減少模型參數的精度(不僅在推理時,訓練過程中也是如此)變得非常具有吸引力,也讓我們這些算力貧民有機會入場 LLM 的遊戲。
然而,這種策略並非沒有代價。
隨著使用的浮點位數減少,模型的準確率通常也會隨之降低,因此如何在不損失準確性的前提下縮減位寬,這就是 Quantization 量化技術在研究的事情!
但如同Day1文章規劃中提到,這個系列文會主要專注在Kaggle解題策略的思考脈絡,關於已經大量應用的模型、演算法等的原理就不會多去著墨。網上有非常多相關的文章和影片在介紹 LLM Quantization 的基本原理與不同技術的差異,在下面整理一些我個人覺得非常優秀、受益頗多的參考資料,有興趣的朋友不妨移步這些資源後再回來接續本文呦!
進入 LLM 的時代後,基本上單張消費級顯卡就不可能再像以前訓練BERT
一樣,做 LLM FFT(Full Fine-Tuning) 了。
這時候,我們就需要借助 LoRA 的力量,也就是一種通過凍結預訓練模型的大部分權重,在模型旁邊外掛一個比較小的模型,訓練時 backward 只更新這個小模型的參數,forward 時則把原本的 pretrained weight 和小模型的結果相加起來當作最終結果的一種計算方式。
透過 LoRA 可以節省訓練模型所花的時間,畢竟只需要更新一小部分的參數,但同時,也可以節省 VRAM 需要的空間呦~
你可能會懷疑,這哪有節省空間?原本有的 Pretrained weights 還是在,然後現在又加上一個外掛小模型,空間不減反增呀!
別著急,推薦大家可以看看【LLM專欄】All about Lora這篇文章,裡面有詳細解釋 LoRA 的原理,當然也可以解決你這個疑惑。
除此之外我也推薦大家參考下面的參考資料,會對 LoRA 有更深入的了解~
總之,在 fine-tune LLM 時,Quantization 和 LoRA 是兩個相輔相成的技術,不僅可以加快訓練與推理速度,也可以減少 LLM 需要的 VRAM 空間,現在有很多訓練 LLM 的框架都會預設使用者會調用這兩個功能。2023 年有一篇 QLoRA 的論文,直接把 Quantization 和 LoRA 結合以來,發展更高效地訓練 LLM 的技術,有興趣的朋友也可以參考上面推薦資料的第四項。
Unsloth 是一個用於加速大模型訓練的開源項目,通過使用 OpenAI 的 Triton 重新 implement 模型計算過程,大幅提升訓練效率並降低VRAM使用,同時保證他們重寫後的模型的計算結果會和原始版本一致,實現中不存在近似計算,模型訓練的精度損失為零。
Unsloth 兼容大多數主流 GPU 設備,如 V100、T4、Titan V、RTX 20、30、40 系列、A100、H100、L40 等,並支持 LoRA 和 QLoRA 的加速訓練與高效顯存管理,同時兼容 Flash Attention 技術。
不過!
現在 Unsloth 目前只支援單卡計算,但這對我們這種沒有實驗室大量硬體運算資源的一般玩家來說,反而不是什麼問題。
一開始我是被他們秀出的成績單吸引:
在Colab T4 上跑 OASST Benchmark,速度比🤗抱抱臉原始的 implementation 快了 1.95 倍接近 2 倍,VRAM 減少了 43.3% ,非常有感地降低硬體負荷,而且這些都是在沒有損失預測精度的情況下,簡直就是天上掉餡餅,讓我們吃了一頓免費的午餐😋!
而且更棒棒的是,Unsloth 和 HuggingFace 的生態兼容,所以我們不用為了使用它,重新學一些複雜的 api,大多可以沿用之前開發寫的代碼。
話不多說,我們就帶大家用 Unsloth 和之前做好的 1K Wiki 擴增版訓練數據集,一起來 Fine-Tune 我們的 LLM 吧!
這次我們選用 Meta-Llama-3.1-8B
的 pretrained LLM 來 fine-tuned。(以下code部分參考自2)
from unsloth import FastLanguageModel
import torch
import pandas as pd
from datasets import Dataset
max_seq_length = 720 # Choose any! We auto support RoPE Scaling internally!
dtype = None # None for auto detection. Float16 for Tesla T4, V100, Bfloat16 for Ampere+
load_in_4bit = True # Use 4bit quantization to reduce memory usage. Can be False.
load_in_4bits
啟用之後就會調用 4 bits quantization 技術,一方面減少 VRAM 用量,一方面加速運算速度。目前應該只有支援 load_in_4bits
如果改成 load_in_8bits
會報錯。
FastLanguageModel
是 unsloth 實作的 model wrapper。model, tokenizer = FastLanguageModel.from_pretrained(
model_name = "unsloth/Meta-Llama-3.1-8B",
max_seq_length = max_seq_length,
dtype = dtype,
load_in_4bit = load_in_4bit,
# load_in_8bit = load_in_4bit,
# token = "hf_...", # use one if using gated models like meta-llama/Llama-2-7b-hf
)
我們可以到 https://huggingface.co/unsloth 這邊去看有哪些支援 4bit pre quantized 的模型。
以前下載 7B, 13B 的模型因為檔案有好幾十GB,下載完真的要等可能半小時以上,但現在選擇這些 pre-quantized 的模型下載會很快,不到五分鐘就結束了。
下面也列出已經 pre-quantized 4 bits 的模型給大家參考:
fourbit_models = [
"unsloth/Meta-Llama-3.1-8B-bnb-4bit", # Llama-3.1 15 trillion tokens model 2x faster!
"unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit",
"unsloth/Meta-Llama-3.1-70B-bnb-4bit",
"unsloth/Meta-Llama-3.1-405B-bnb-4bit", # We also uploaded 4bit for 405b!
"unsloth/Mistral-Nemo-Base-2407-bnb-4bit", # New Mistral 12b 2x faster!
"unsloth/Mistral-Nemo-Instruct-2407-bnb-4bit",
"unsloth/mistral-7b-v0.3-bnb-4bit", # Mistral v3 2x faster!
"unsloth/mistral-7b-instruct-v0.3-bnb-4bit",
"unsloth/Phi-3.5-mini-instruct", # Phi-3.5 2x faster!
"unsloth/Phi-3-medium-4k-instruct",
"unsloth/gemma-2-9b-bnb-4bit",
"unsloth/gemma-2-27b-bnb-4bit", # Gemma 2x faster!
]
FastLanguageModel
的 get_peft_model()
method,把剛剛宣告的模型以及 LoRA 相關的設定參數傳進去,這樣回傳出來的 model 就會是加上外掛的 model 囉!model = FastLanguageModel.get_peft_model(
model,
r = 16, # Choose any number > 0 ! Suggested 8, 16, 32, 64, 128
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",],
lora_alpha = 16,
lora_dropout = 0, # Supports any, but = 0 is optimized
bias = "none", # Supports any, but = "none" is optimized
# [NEW] "unsloth" uses 30% less VRAM, fits 2x larger batch sizes!
use_gradient_checkpointing = "unsloth", # True or "unsloth" for very long context
random_state = 3407,
use_rslora = False, # We support rank stabilized LoRA
loftq_config = None, # And LoftQ
)
我們來逐個解釋一下上面設定到的和 LoRA 有關的參數:r
: 就是 rank,這個參數決定了 LoRA 矩陣的大小。Rank 通常從 8 開始,最多可以設定到 256。雖然較高的 rank 可以存儲更多信息,但會增加 LoRA 的計算和內存成本。我們在這里將其設置為預設值 16。target_module
: LoRA 既然是一個外掛,那我們就可以決定這個外掛要放在模型的哪些 module,包含可以放在 self-attention 層的 Q, K, V, O 矩陣旁邊,或是前饋神經網路的 up/down 投影曾旁邊。加越多地方,要訓練的參數量和內存需求就會越大。這邊我們預設全都加。lora_alpha
: Alpha 直接影響這個外掛 adapter 的貢獻,通常會設定成 r
的兩倍,實驗下來這樣效果會比較好。use_rslora
: 如果打開的話,就會使用 Rank Stabilized LoRA,會讓 LoRA 訓練的時候更穩定,詳細可參考:Rank-Stabilized LoRA: Unlocking the Potential of LoRA Fine-Tuning
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
{}
### Input:
{}
### Response:
{}"""
EOS_TOKEN = tokenizer.eos_token # Must add EOS_TOKEN
# %%
def formatting_prompts_func(examples):
instructions = examples["instruction"]
inputs = examples["input"]
outputs = examples["output"]
texts = []
for instruction, input, output in zip(instructions, inputs, outputs):
# Must add EOS_TOKEN, otherwise your generation will go on forever!
text = alpaca_prompt.format(instruction, input, output) + EOS_TOKEN
texts.append(text)
return { "text" : texts, }
pass
instruction 的部分放我們自己設計的 prompt:
df['instruction'] = "Here is a multiple-choice question generated from a Wikipedia page on a science-related topic. Each question has five options. After reading the question and the five options, please respond with 'option_1', 'option_2', 'option_3', 'option_4', or 'option_5' to indicate the correct answer. Only provide the answer without any additional explanation. You must select one option, and blank responses are not allowed."
整理成固定格式的 dataset:
trainset 放的是我們自己擴增的那 1000 筆資料,testset 則是 Host 提供的那 200 筆有 ground truth 的資料:
trainset = Dataset.from_pandas(df_train)
trainset =trainset.map(formatting_prompts_func, batched=True)
testset = Dataset.from_pandas(df)
testset =testset.map(formatting_prompts_func, batched=True)
from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported
trainer = SFTTrainer(
model = model,
tokenizer = tokenizer,
train_dataset = trainset,
eval_dataset = testset,
dataset_text_field = "text",
max_seq_length = max_seq_length,
dataset_num_proc = 2,
packing = False, # Can make training 5x faster for short sequences.
args = TrainingArguments(
per_device_train_batch_size = 2,
gradient_accumulation_steps = 4,
warmup_steps = 5,
num_train_epochs = 1, # Set this for 1 full training run.
# max_steps = 60,
learning_rate = 2e-4,
fp16 = not is_bfloat16_supported(),
bf16 = is_bfloat16_supported(),
logging_steps = 1,
eval_steps = 10,
evaluation_strategy = "steps",
optim = "adamw_8bit",
weight_decay = 0.01,
lr_scheduler_type = "linear",
seed = 3407,
output_dir = "outputs",
),
)
接下來,開始訓練:
trainer_stats = trainer.train()
我使用單張 RTX 4090 24GB 的顯卡,經過實測,峰值顯存用量為13.292GB.
* 訓練框架: unsloth
* 訓練模型: Meta-Llama-3.1-8B
* 訓練8B模型,最大token長度720,Batch Size 設定為 4,峰值顯存為10.292 GB.
* 1000 筆資料,訓練 1 個 epoch,大概三分鐘跑完。
FastLanguageModel.for_inference(model)
然後定義一個 generate function:
def generate_outputs(examples):
# 建立批量的 prompts
prompts = [
alpaca_prompt.format(instruction, input_data, "")
for instruction, input_data in zip(examples["instruction"], examples["input"])
]
# 將 prompts 轉換為模型的輸入格式
inputs = tokenizer(prompts, return_tensors="pt", padding=True, truncation=True).to("cuda")
# 使用模型進行生成
outputs = model.generate(**inputs, max_new_tokens=5, do_sample=False, use_cache=True)
# 將生成結果解碼
decoded_outputs = tokenizer.batch_decode(outputs, skip_special_tokens=True)
# 過濾掉 prompts,僅保留生成的 tokens
generated_texts = []
for prompt, decoded_output in zip(prompts, decoded_outputs):
# 刪除 prompt 的部分,只保留其後的生成結果
generated_text = decoded_output[len(prompt):].strip()
generated_texts.append(generated_text)
return {"generated_output": generated_texts}
最後,我們就可以在 testset 上生成模型的預測結果:
testset = testset.map(generate_outputs, batched=True, batch_size=8, desc="Generating outputs")
為了瞭解 Meta-Llama-3.1-8B
到底訓練後跟原本有沒有差?
我首先讓 Meta-Llama-3.1-8B
在那兩百筆 testset 上先裸考一遍,看它的準確率如何,接下來再用那 1000 筆資料,從隨機抽取 200, 400, 600, 800 到 1000 筆資料全下,用不同資料量訓練5個模型,觀察他們的 loss curve 以及 accuracy 的變化。
我們從 validation loss 可以觀察到,隨著訓練資料量增加,validation loss 也會逐漸下降。都是訓練 1 個 epoch,全部資料下去硬 train 一發的模型,validation loss 來到最低點 0.4986 左右。
那 Accuracy 的表現呢?
Method | Accuracy |
---|---|
模型裸考 | 42.5% |
200 筆資料訓練 | 55% |
400 筆資料訓練 | 55.5% |
600 筆資料訓練 | 58.5% |
800 筆資料訓練 | 60% |
1000 筆資料訓練 | 61% |
在 Accuracy 上也表現出隨著訓練資料增加,模型持續增益的現象~
到這邊,過渡章節終於結束了!
明天,我們就會開始進入本賽題的金牌作法解析囉🤓!
我們明天見!
謝謝讀到最後的你,希望你會覺得有趣!
如果喜歡這系列,別忘了按下訂閱,才不會錯過最新更新,也可以按讚⭐️給我鼓勵唷!
如果有任何回饋和建議,歡迎在留言區和我說✨✨
(Kaggle - LLM Science Exam 解法分享系列)